多线程学习


其中继承 Thread 类和实现 Runnable 接口是最基本的方式,但有一个共同的缺点 ---- 没有返回值。而 FutureTask 则解决了这个问题。Executor 是 JDK 提供的多线程框架,功能十分强大.

1.继承 Thread 类

public class Punishment {
    private int leftCopyCount;
    private String wordToCopy;
}

//1、继承Thread类
public class Student extends Thread{
    private String name;
    private Punishment punishment;

    public Student(String name, Punishment punishment) {
        //2、调用Thread构造方法,设置threadName
        super(name);
        this.name=name;
        this.punishment = punishment;
    }

    public void copyWord() {
        int count = 0;
        String threadName = Thread.currentThread().getName();

        while (true) {
                if (punishment.getLeftCopyCount() > 0) {
                    int leftCopyCount = punishment.getLeftCopyCount();
                    System.out.println(threadName+"线程-"+name + "抄写" + punishment.getWordToCopy() + "。还要抄写" + --leftCopyCount + "次");
                    punishment.setLeftCopyCount(leftCopyCount);
                    count++;
                } else {
                    break;
                }
        }
        System.out.println(threadName+"线程-"+name + "一共抄写了" + count + "次!");
    }
    //3、重写run方法,调用copyWord完成任务
    @Override
    public void run(){
        copyWord();
    }
}

public class StudentClient {
    public static void main(String[] args) {
        Punishment punishment = new Punishment(100,"internationalization");
        Student student = new Student("小明",punishment);
        student.start();
    }
}

Thread类的api

sleep 方法
线程休眠指定的时间长度。该线程持有的 monitor 锁并不会被放弃。
sleep 方法有两个重载,分别是

public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException

第二个实现
public static void sleep(long millis, int nanos)
throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }
    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }
    sleep(millis);
}

最终调用的还是第一个毫秒级别的 sleep 方法。而传入的纳秒会被四舍五入。如果大于 50 万,毫秒 ++,否则纳秒被省略。

yield 方法
yield 单词的意思是让路,在多线程中意味着本线程愿意放弃 CPU 资源,也就是可以让出 CPU 资源。不过这只是给 CPU 一个提示,当 CPU 资源并不紧张时,则会无视 yield 提醒。如果 CPU 没有无视 yield 提醒,那么当前 CPU 会从 RUNNING 变为 RUNNABLE 状态,此时其它等待 CPU 的 RUNNABLE 线程,会去竞争 CPU 资源。
yield 方法为了提升线程间的交互,避免某个线程长时间过渡霸占 CPU 资源。但 yield 在实际开发中用的比较少,源码的注解也提到这一点:“It is rarely appropriate to use this method.”。

currentThread 方法
这是一个静态方法,用于获取当前线程的实例

Thread.currentThread();

Thread.currentThread().getName();

Thread.currentThread().getId();

setPriority 方法
用于设置线程的优先级。每个线程都有自己的优先级数值,当 CPU 资源紧张的时候,优先级高的线程获得 CPU 资源的概率会更大。
Thread 有自己的最小和最大优先级数值,范围在 1-10。如果不在此范围内,则会报错。另外如果设置的 priority 超过了线程所在组的 priority ,那么只能被设置为组的最高 priority 。最后通过调用 native 方法 setPriority0 进行设置。

interrupt 相关方法
interrupt 的意思是打断。中断的并不是线程的逻辑,中断的是线程的阻塞,如让 sleep 中断。
调用 interrupt 方法,并不会影响可中断方法之外的逻辑。线程不会中断,会继续执行。这里的中断概念并不是指中断线程。
一旦调用了 interrupt 方法,那么线程的 interrupted 状态会一直为 ture(没有通过调用可中断方法或者其他方式主动清除标识的情况下。

join 方法
实现并行化处理,主线程需要做两件没有相互依赖的事情,那么可以起 A、B 两个线程分别去做。通过调用 A、B 的 join 方法,让主线程 block 住,直到 A、B 线程的工作全部完成,才继续走下去。

wait/notify概念

这两个方法并不在 Thread 对象中,而是在 Object 中。也就是说所有的 Java 类都继承了这两个方法。所有 Java 类都会继承这两个方法的原因是 Java 中同步操作的需要。
原本 RUNNING 的线程,可以通过调用 wait 方法,进入 BLOCKING 状态。此线程会放弃原来持有的锁。而调用 notify 方法则会唤醒 wait 的线程,让其继续往下执行。

2.实现 Runnable 接口

public class Student implements Runnable{
    private String name;
    private Punishment punishment;

    public Student(String name, Punishment punishment) {
        this.name=name;
        this.punishment = punishment;
    }

    public void copyWord() {
        int count = 0;
        String threadName = Thread.currentThread().getName();

        while (true) {
                if (punishment.getLeftCopyCount() > 0) {
                    int leftCopyCount = punishment.getLeftCopyCount();
                    System.out.println(threadName+"线程-"+name + "抄写" + punishment.getWordToCopy() + "。还要抄写" + --leftCopyCount + "次");
                    punishment.setLeftCopyCount(leftCopyCount);
                    count++;
                } else {
                    break;
                }
        }

        System.out.println(threadName+"线程-"+name + "一共抄写了" + count + "次!");
    }

    //重写run方法,完成任务。
    @Override
    public void run(){
        copyWord();
    }
}

public class StudentClient {
    public static void main(String[] args) {
        Punishment punishment = new Punishment(100,"internationalization");
        Thread xiaoming = new Thread(new Student("小明",punishment),"小明");
        xiaoming.start();
    }
}
Thread和Runnable区别

Thread:
(1)继承Thread类。
(2)调用Thread构造方法,设置threadName。
(3)调用 Thread 对象的 start 方法来启动线程。
(4)run 方法实现在 Thread 子类中,线程启动后,是 thread 对象运行自己的 run 方法逻辑。
Runnable:
(1)main方法要新创建一个Thread类,把实现了 runnable 接口的对象通过构造函数传递进去,Thread 构造函数的第二个参数是自定义的 thread name,由于 Student 就是 Thread 的子类,所以我们直接通过 new Student 就可以得到线程对象。
(2)调用 Thread 对象的 start 方法来启动线程。
(3)run方法逻辑转移到 Runnable 的实现类中,线程启动后,是调用 Runnable 实现的 run 方法逻辑。

为什么实现Runnable比继承Thread好?

(1)java语言中只能单继承,通过实现接口的方式,可以让实现类去继承其它类。而直接继承thread就不能再继承其它类了。
(2)线程控制逻辑在Thread类中,业务运行逻辑在Runnable实现类中,解耦更为彻底。
(3)实现Runnable的实例,可以被多个线程共享并执行。而实现thread是做不到这一点的。

Runnable的run方法是何时被调用的?
//Thread类的构造方法
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
//init方法,下面有一行赋值的代码
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals)
this.target = target;

//Runnable是Thread的成员变量
/* What will be run. */
private Runnable target;

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210624201559165.png
由于start0是一个native方法,所以后面的执行会进入到JVM中。

3.Executor

实际上我们使用的线程池的实现是 ThreadPoolExecutor。它实现了线程池工作的完整机制。
corePoolSize 即线程池的核心线程数量,其实也是最小线程数量。
maximumPoolSize 即线程池的最大线程数量。受限于线程池的 CAPACITY。线程池的 CAPACITY 为 2 的 29 次方 -1。这是由于线程池把线程数量和状态保存在一个整形原子变量中。状态保存在高位,占据了两位,所以线程池中线程数量最多到 2 的 29 次方 -1。

public class Client {
    public static Executor executor = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        Stream.iterate(1, item -> item + 1).limit(20).forEach(item -> {
                    executor.execute(() -> {
                        System.out.println(Thread.currentThread().getName() + " hello!");
                    });
                }
        );
    }
}

4.Future

主线程只要声明了 Future 对象,并且启动新的线程运行他。那么随时能通过 Future 对象获取另外线程运行的结果。
1、FutureTask 实现 Runnable 和 Future 接口;
2、在线程上运行 FutureTask 后,run 方法被调用,run 方法会调用传入的 Callable 接口的 call 方法;
3、拿到返回值后,通过 set 方法保存结果到 outcome,并且唤醒所有等待的线程;
4、调用 get 方法获取执行结果时,如果没有执行完毕,则进入等待,直到 set 方法调用后被唤醒。

线程的生命周期

在这里插入图片描述
新建状态(NEW):Thread 对象刚刚被创建时,状态为 NEW。
就绪状态(RUNNABLE):调用start方法后,等待CPU调度。RUNNABLE 状态的线程只可能变迁为 RUNNING 和 TERMINATED 状态。当等到 CPU 调度时,状态变为 RUNNING。而在等待 CPU 调度期间,如果被意外终止,那么则会直接进入 TERMINATED 状态。
运行状态(RUNNING):CPU 选中执行,他就会变为 RUNNING 状态。此时才会真正运行 run 方法逻辑。RUNNING 状态的线程可以变迁为如下状态:
(1)BLOCKED 状态
(2)RUNNING 的线程如果在执行过程中调用了 wait 或者 sleep 方法,就会进入 BLOCKED 状态
(3)阻塞的 I/O 操作
(4)进入到锁的阻塞队列
(5)TERMINATED
(6)run 方法正常执行结束
(7)运行意外中止
(8)调用 stop 方法(已经不推荐使用)
(9)RUNNABLE
(10)CPU 轮转,放弃该线程的执行
(11)调用了 Thread 的 yield 方法,提示 CPU 可以让出自己的执行权。如果 CPU 对此响应,则会进入到 RUNNABLE 状态,而不是 TERMINATED。
阻塞状态(BLOCKED):只有 RUNNING 状态的线程才会进入 BLOCKED 状态。处于 BLOCKED 状态的线程,可以转换为如下状态:
(1)RUNNABLE 状态
(2)阻塞操作结束了,那么线程将切换回 RUNNABLE 状态。注意不是 RUNNING 状态,此时线程需要等待 CPU 的再一次选中。
(3)阻塞过程中被打断了,由于其它线程调用了 interrupt 方法,也会回到 RUNNABLE 状态。
(4)线程休眠的时间结束了,也会回到 RUNNABLE 状态。
(5)由于 wait 操作进入 BLOCKED 状态的线程,其他线程发出了 notify 或 notifyAll ,则会唤醒它,回到 RUNNABLE 状态。
(6)由于等待锁而被 BLOCKED 的线程。一旦获取了锁,那么便会回到 RUNNABLE 状态。
(7)TERMINATED 状态
(8)线程 BLOCKED 状态时,有可能由于调用了 stop 或者意外终止,而直接进入了 TERMINATED 状态。
结束状态(TERMINATED):TERMINATED 状态意味着线程的生命周期已经走完。这是线程的终止状态。此状态的线程不会再转化为其它任何状态。处于 RUNNING 或者 BLOCKED 状态的线程都有可能变为 TERMINATED 状态,但原因是类似的,如下:
(1)线程运行正常结束
(2)程序运行异常终止
(3)JVM 意外终止

守护线程

当 JVM 中没有任何一个非守护线程时,所有的守护线程都会进入到 TERMINATED(结束) 状态,JVM 退出。守护线程一般用于执行独立的后台业务。比如 JAVA 的垃圾清理就是由守护线程执行。而所有非守护线程都退出了,也没有垃圾回收的需要了,所以守护线程就随着 JVM 关闭一起关闭了。要实现守护线程只能手动设置,在线程 start 前调用 setDaemon 方法。Thread 没有直接创建守护进程的方式,非守护线程创建的子线程都是非守护线程。

并发编程的三大特性

原子性
所有操作要么全部成功,要么全部失败。
可见性
一个线程对变量进行了修改,另外一个线程能够立刻读取到此变量的最新值。
变量被修改后,在本线程中确实能够立刻被看到,但并不保证别的线程会立刻看到。原因就是编程领域经典的两大难题之一----缓存一致性。
有序性
代码在运行期间保证按照编写的顺序。
volatile 修饰的变量是会保证读操作一定能读到写完的值。
有序性是在多线程的情况下,确保 CPU 不对我们需要保证顺序性的代码进行重排序的。我们可以通过 sychronized 或者 volatile 来确保有序性。

Atomic 简介

原子性的轻量级实现。
AtomicInteger 提供的 incrementAndGet () 方法,则把这两步操作作为一个原子性操作来完成,则不会出现线程安全问题。
Atomic 变量的操作使用了 CAS 算法保证原子性。

CAS 算法分析(Compare and swap)

CAS 是乐观锁的一种实现,Synchronized 则是悲观锁。
悲观锁–认为每一次自己的操作大概率会有其它线程在并发,所以自己在操作前都要对资源进行锁定,这种锁定是排他的。悲观锁的缺点是不但把多线程并行转化为了串行,而且加锁和释放锁都会有额外的开支。
乐观锁–认为每一次操作时大概率不会有其它线程并发,所以操作时并不加锁,而是在对数据操作时比较数据的版本,和自己更新前取得的版本一致才进行更新。乐观锁省掉了加锁、释放锁的资源消耗,而且在并发量并不是很大的时候,很少会发生版本不一致的情况,此时乐观锁效率会更高。
Atomic 变量在做原子性操作时,会从内存中取得要被更新的变量值,并且和你期望的值进行比较,期望的值则是你要更新操作的值。如果两个值相等,那么说明没有其它线程对其更新,本线程可以继续执行。如果不等,说明有线程已经先于此线程进行了更新操作。那么则继续取得该变量的最新值,重复之前的逻辑,直至操作成功。这保证了每个线程对 Atomic 变量操作是线程安全的。
加了 Lock 前缀的操作,在执行期间,所使用的缓存会被锁定,其他处理器无法读写该指令要访问的内存区域,由此保证了比较替换的原子性。而这个操作过程称之为缓存锁定

CAS 的缺点:
(1)比较替换如果失败,则会一直循环,直至成功。这在并发量很大的情况下对 CPU 的消耗将会非常大;
(2)只能保证一个变量自身操作的原子性,但多个变量操作要实现原子性,是无法实现的;
(3)ABA 问题。假如本线程更新前取得期望值为 A,和更新操作之间的这段时间内,其它线程可能把 value 改为了 B 又改回了 A。 而本线程更新时发现 value 和期望值一样还是 A,认为其没有变化,则执行了更新操作。但其实此时的 A 已经不是彼时的 A 了。大多数情况下 ABA 不会造成业务上的问题。但是如果你认为 ABA 问题对你的程序业务有问题,那么就需要解决。 JDK 提供了 AtomicStampedReference 类,通过对 Atomic 包装的变量增加版本号,来解决 ABA 问题,即使 value 还是 A,但如果版本变化了,也认为比较失败。

volatile 关键字

第一种方法就是解决一切并发问题的方法–同步。不过读和写都需要同步。
第二种方法会简单很多,使用 volatile 关键字。
volatile 修饰的变量,在发生变化的时候,其它线程会立刻觉察到,然后从主存中取得更新后的值。volatile 除了简洁外,还有个好处就是它不会加锁,所以不会阻塞代码。
volatile 关键字可以用来修饰实例变量和类变量。被 volatile 修饰后,该变量或获得以下特性:
(1)可见性。任何线程对其修改,其它线程马上就能读到最新值。
(2)有序性。禁止指令重排序。
lock 的作用是在其有效的范围内锁住总线,从而执行该行代码线程所在的处理器能够独占资源。由于总线被锁定,开销很大的。所以新的 CPU 实现已经不会锁住总线,而是锁定变量所在的缓存区域,就像上文描述的 MESI 协议,从而保证了数据的可见性。
volatile 的有序性则是通过内存屏障。所谓的内存屏障就是在屏障前的所有指令可以重排序的,屏障之后的指令也可以重排序,但是重排序的时候不能越过内存屏障。也就是说内存屏障前的指令不会被重排序到内存屏障之后,反之亦然。
volatile 能够保证变量的可见性和有序性,但是并不能保证原子性。
volatile 能为我们提供如下特性:
确保实例变量和类变量的可见性。
确保 volatile 变量前后代码的重排序以 volatile 变量为界限。
volatile 的局限性:
volatile 的可见性和有序性只能作用于单一变量。
volatile 不能确保原子性。
volatile 不能作用于方法,只能修饰实例或者类变量。
volatile 的以上特点,决定了它的使用场景是有限的,并不能完全取代 synchronized 同步方式。一般使用 volatile 的场景是代码中通过某个状态值 flag 做判断,flag 可能被多个线程修改。如果不使用 volatile 修饰,那么 flag 不能保证最新的值被每个线程读取到。而在使用 volatile 修饰后,任何线程对 flag 的修改,都立刻对其它线程可见。此外其它线程看到 flag 变化时,所有对 flag 操作前的代码都已生效,这是 volatile 的有序性确保的。
volatile 不适用的场景如下:
一个变量或者多个变量的原子性操作。
不以 volatile 变量操作作为分界线的有序性保证。

MESI 协议

CPU 为了提升速度,采用了缓存,因此造成了多个线程缓存不一致的问题,这也是可见性的根源。为了解决缓存一致性,我们需要了解缓存一致性协议。MESI 协议是目前主流的缓存一致性协议。此协议会保证,写操作发生时,线程独占该变量的缓存,CPU 并且会通知其它线程对于该变量所在的缓存段失效。只有在独占操纵完成之后,该线程才能修改此变量。而此时由于其它缓存全部失效,所以就不存在缓存一致性问题。而其它线程的读取操作,需要等写入操作完成,恢复到共享状态。

synchronized概念

关键字需要配合一个对象使用,其实这个对象可以是任何对象。其实 synchronized 所使用的对象,只是用来记录等待同步操作的线程集合。他相当于一位排队管理员,所有线程都要在此排队,并接受他的管理,他说谁能进就可以进。另外他维护了一个 wait set,所有调用了 wait 方法的线程都保存于此。一旦有线程调用了同步对像的 notify 方法,那么 wait set 中的线程就会被 notify,继续执行自己的逻辑。
需要注意的是,我们对哪个对象做了 synchronized 操作,那么就只能在同步代码块中使用此对象进行 wait 和 notify 的操作。
通过 wait/notifyAll 让多个线程交互,同时通过共享资源的状态,各线程控制自己的逻辑。这样的程序称之为状态驱动程序。也就是说是否真的执行逻辑,是由状态值所决定的。如果状态不满足,即使被 notify 了,也会再次进入 wait set。
synchronized 的使用非常简单,有两种方式,
第一种是同步代码块。
第二种使用 synchronized 关键字修饰方法。

public synchronized void eat(){
	.......
  .......
}

public void eat(){
	synchronized(this){
		.......
  	.......
	}
}

每个对象都关联了一个 monitor lock。
当一个线程获取了 monitor lock 后,其它线程如果运行到获取同一个 monitor 的时候就会被 block 住。当这个线程执行完同步代码,则会释放 monitor lock。在后一个线程获取锁后,happens-before 原则生效,前一个线程所做的任何修改都会被这个线程看到。
我们再深入底层一点来分析。每个 Java 对象在 JVM 的对等对象的头中保存锁状态,指向 ObjectMonitor。ObjectMonitor 保存了当前持有锁的线程引用,EntryList 中保存目前等待获取锁的线程,WaitSet 保存 wait 的线程。此外还有一个计数器,每当线程获得 monitor 锁,计数器 +1,当线程重入此锁时,计数器还会 +1。当计数器不为0时,其它尝试获取 monitor 锁的线程将会被保存到EntryList中,并被阻塞。当持有锁的线程释放了monitor 锁后,计数器 -1。当计数器归位为 0 时,所有 EntryList 中的线程会尝试去获取锁,但只会有一个线程会成功,没有成功的线程仍旧保存在 EntryList 中。由此可以看出 monitor 锁是非公平锁。
synchronized 使用注意
synchronized 使用的为非公平锁,如果你需要公平锁,那么不要使用 synchronized。可以使用 ReentrantLock,设置为公平锁。关于 ReentrantLock,会在后面章节进行讲解;
锁对象不能为 null。如果锁对象为 null,何谈对象头,以及保存与其关联的 monitor 锁呢?所以代码中要确保synchronized使用的锁对象不为 null;
只把需要同步的代码放入 synchronized 代码块。如果不思考,为了线程安全把方法中全部代码都放入同步代码块,那么将会丧失多线程的优势。再多的线程也只能串行执行,这完全违背了并发的初衷;
只有使用同一个对象作为锁对象,才能同步。记住是同一个对象,而不是同一个类。有一种常犯的错误是,不同线程持有的是同一个类的不同实例。那么该对象实例用作锁对象的话,多个线程并不会同步。还一种错误是使用不同类的实例作为锁对象,但是期望不同位置的同步代码块能够同步执行。这是不可能达到你想要的效果的。

指令重排序

CPU 为了提高运行效率,可能会对编译后代码的指令做一些优化,这些优化不能保证 100% 符合你编写代码在正常编译后的顺序执行。但是一定能保证代码执行的结果和按照编写顺序执行的结果是一致的。
指令重排序并不是毫无约束的随意改变代码执行顺序,而是需要符合指令间的依赖关系,否则会造成程序执行结果错误。
指令重排序的优化,仅仅对单线程程序确保安全。如果在并发的情况下,程序没能保证有序性,程序的执行结果往往会出乎我们的意料。另外注意,指令重排序,并不是代码重排序。我们的代码被编译后,一行代码可能会对应多条指令,所以指令重排序更为细粒度。

public class Singleton {
    private static Singleton aaa; 
    private Singleton (){
    }
 
    public static Singleton getSingleton() {
        if (aaa == null) {                         
            synchronized (Singleton.class) {
                if (aaa == null) {       
                    aaa = new Singleton();
                }
            }
        }
        return aaa;
    }
   
}

竞态条件

竞态条件是指,在多线程的情况下,由于多个线程执行的时序不同,而出现不正确的结果。
如果在需要保证原子性的一组操作中,有竞态条件产生,那么就会出现线程安全的问题。我们可以通过为原子操作加锁或者使用原子变量来解决。原子变量在 java.util.concurrent.atomic 包中,它提供了一系列的原子操作。

CPU 缓存模型

CPU 用了 L1、L2、L3,一共三级缓存。其中 L1 缓存根据用途不同,还分为 L1i 和 L1d 两种缓存。

最低安全性

虽然线程在未做同步的时候会读取到失效值,但是起码这个值是曾经存在过的。这称之为最低安全性。
对于 64 位类型的变量 long 和 double,JVM 会把读写操作分解为两个 32 位的操作。如果两个线程分别去读和写,那么在读的时候,可能写线程只修改了一个 32 位的数据。此时读线程会读取到原来数值一个 32 位的数值和新的数值一个 32 位的数值。两个不同数值各自的一个 32 位数值合在一起会产生一个新的数值,没有任何线程设置过的数值。这就好比马和驴各一半的基因,会生出骡子一样。此时,就违背了最低安全性。

JAVA内存模型即JMM(Java Memory Model)

它描述了Java程序的运行行为,包括多线程操作对共享内存读取时,所能读取到的值应该遵守的规则。JMM使得Java程序能够在任何JVM上表现出一样的行为。
JMM为程序中的所有操作定义了一定的规则,叫做Happens-Before。无论两个操作是否在同一个线程,如果要想保证操作A能看到操作B的结果,那么A、B之间一定要满足Happens-Before关系。如果两者间不满足Hapen-Before关系,JVM可以对其任意重排序。
当多个线程同时读写同一个变量,但这些操作间又没有满足Happens-Before关系,那么这些线程对此变量存在数据竞争,整个程序将会陷入混乱之中。假如我们在操作共享变量时采用了同步,那么无论有多少线程,对此变量的操作都会呈现出串行一致性。从而使得多线程的操作顺序遵守JMM约定。

Happens-Before规则

Happens-Before在多线程领域具有重大意义,它可以指导你如何开发多线程的程序,而不至于陷入混乱之中。你所开发的多线程程序,如果想对共享变量的操作符合你设想的顺序,那么需要依照Happens-Before原则来开发。happens-before并不是指操作A先于操作B发生,而是指操作A的结果在什么情况下可以被后面操作B所获取。下面我们就来看一下Happens-before原则。
(1)程序顺序规则。如果程序中A操作在B操作之前,那么线程中A操作将在B操作前执行。
(2)上锁原则。不同线程对同一个锁的lock操作一定在unclock前。
(3)volatile变量原则。对于volatile变量的写操作会早于对其的读操作。
(4)线程启动原则。A线程中调用threadB.start()方法,那么threadB.start()方法会早于B线程中中的任何动作执行。
(5)传递规则。如果A早于B执行,B早于C执行,那么A一定早于C执行。
(6)线程中断规则:线程interrupt()方法的一定早于检测到线程的中断信号。
(7)线程终结规则:如果线程A终结了,并且导致另外一个线程B中的ThreadA.join()方法取得返回,那么线程A中所有的操作都早于线程B在ThreadA.join()之后的动作发生。
(8)对象终结规则:一个对象初始化操作肯定先于它的finalize()方法。

ThreadLocal

ThreadLocal中存储的变量是线程隔离的。
1、存储需要在线程隔离的数据。比如线程执行的上下文信息,每个线程是不同的,但是对于同一个线程来说会共享同一份数据。Spring MVC的 RequestContextHolder 的实现就是使用了ThreadLocal。
2、跨层传递参数。层次划分在软件设计中十分常见。层次划分后,体现在代码层面就是每层负责不同职责,一个完整的业务操作,会由一系列不同层的类的方法调用串起来完成。有些时候第一层获得的一个变量值可能在第三层、甚至更深层的方法中才会被使用。如果我们不借助ThreadLocal,就只能一层层地通过方法参数进行传递。使用ThreadLocal后,在第一层把变量值保存到ThreadLocal中,在使用的层次方法中直接从ThreadLocal中取出,而不用作为参数在不同方法中传来传去。不过千万不要滥用ThreadLocal,它的本意并不是用来跨方法共享变量的。结合第一种情况,我们放入ThreadLocal跨层传递的变量一般也是具有上下文属性的。比如用户的信息等。这样我们在AOP处理异常或者其他操作时可以很方便地获取当前登录用户的信息。

一般来说,在实践中,我们会把ThreadLocal对象声名为static final,作为私有变量封装到自定义的类中。另外提供static的set和get方法。
这样做的目的有二:
1、static 确保全局只有一个保存OperationInfoDTO对象的ThreadLocal实例。
2、final 确保ThreadLocal的实例不可更改。防止被意外改变,导致放入的值和取出来的不一致。另外还能防止ThreadLocal的内存泄漏。

set时,会把当前threadLocal对象作为key,你想要保存的对象作为value,存入map。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的静态内部类。在ThreadLocalMap中使用WeakReference包装后的ThreadLocal对象作为key,也就是说这里对ThreadLocal对象为弱引用。当ThreadLocal对象在ThreadLocalMap引用之外,再无其他引用的时候能够被垃圾回收。

如果ThreadLocal对象被回收,那么ThreadLocalMap中保存的key值就变成了null,而value会一直被Entry引用,而Entry又被threadLocalMap对象引用,threadLocalMap对象又被Thread对象所引用,那么当Thread一直不终结的话,value对象就会一直驻留在内存中,直至Thread被销毁后,才会被回收。这就是ThreadLocal引起内存泄漏问题。
而ThreadLocalMap在设计的时候也考虑到这一点,在get和set的时候,会把遇到的key为null的entry清理掉。不过这样做也不能100%保证能够清理干净。我们可以通过以下两种方式来避免这个问题:
1、把ThreadLocal对象声明为static,这样ThreadLocal成为了类变量,生命周期不是和对象绑定,而是和类绑定,延长了声明周期,避免了被回收;
2、在使用完ThreadLocal变量后,手动remove掉,防止ThreadLocalMap中Entry一直保持对value的强引用。导致value不能被回收。

ReentrantLock

ReentrantLock 的使用相比较 synchronized 会稍微繁琐一点,所谓显示锁,也就是你在代码中需要主动的去进行 lock 操作。
lock.lock () 就是在显式的上锁。上锁后,下面的代码块一定要放到 try 中,并且要结合 finally 代码块调用 lock.unlock () 来释放锁,否则一定 doSomething 方法中出现任何异常,这个锁将永远不会被释放掉。
ReentrantLock 的设计思想是通过 FIFO 的队列保存等待锁的线程。通过 volatile 类型的 state 保存锁的持有数量,从而实现了锁的可重入性。而公平锁则是通过判断自己是否排队成功,来决定是否去争抢锁。

公平锁和非公平锁

synchronized 是非公平锁,也就是说每当锁匙放的时候,所有等待锁的线程并不会按照排队顺去依次获得锁,而是会再次去争抢锁。ReentrantLock 相比较而言更为灵活,它能够支持公平和非公平锁两种形式。只需要在声明的时候传入 true。

Lock lock = new ReentrantLock(true);
默认的无参构造方法则会创建非公平锁。

公平性的选择,意味着需要放弃一部分性能。大多数情况下,公平锁的性能都要低于非公平锁。这是因为挂起和恢复线程都有很大开销。选择公平锁时,从释放锁到等待队列中最前面线程被唤醒能够去 tryLock,中间有很大的时间延迟,那么这就造成了公平锁的性能会更差。

tryLock

前面我们通过 lock.lock (); 来完成加锁,此时加锁操作是阻塞的,直到获取锁才会继续向下进行。ReentrantLock 其实还有更为灵活的枷锁方式 tryLock。tryLock 方法有两个重载,第一个是无参数的 tryLock 方法,被调用后,该方法会立即返回获取锁的情况。获取为 true,未能获取为 false。我们的代码中可以通过返回的结果进行进一步的处理。第二个是有参数的 tryLock 方法,通过传入时间和单位,来控制等待获取锁的时长。如果超过时间未能获取锁则放回 false,反之返回 true。

ReentrantLock和synchronized区别

ReentrantLock 支持公平锁,而内置锁不能支持公平锁。ReentrantLock 内部有一个线程排队的队列,如果 ReentrantLock 选择了公平的方式,那么队列中的线程会按照顺序去 tryLock。非公平的方式,在锁释放后,如果有新的线程来竞争锁,那么就可能插队,在等待队列中的线程被恢复并获取锁之前,新的线程获取了锁。

Exchanger

用来在线程间交换数据。如果两个线程并行处理,但在某个时刻需要互相交换自己已经处理完的中间数据,然后才能继续往下执行。这个时候就可以使用 Exchanger。

线程池

线程池的作用是维护一定数量的线程,接收任意数量的任务,这些任务被线程池中的线程并发执行。
Java 提供了 Excutor 来实现线程池。

死锁

死锁,其实就是因为某种原因,达不到解锁的条件,导致某线程对资源的占有无法释放,其他线程会一直等待其解锁,而被一直 block 住。
(1)交叉死锁:两个线程互相占且想得到有对方的资源,互相不肯释放锁。
(2)内存不足:某系统内存 20M,两个线程正在分别执行任务,各自已经使用了 10M 内存。但是执行到一半时需要更大的内存,但是系统已经没有内存可供使用。那么两个线程都会等待对方执行完毕 时释放内存。这就造成了两个线程互相等待,从而形成死锁。
(3)一问一答式的数据交换:所谓的一问一答式数据交换就是客户端发送请求,服务端返回响应。如果在交互过程中出现了数据的丢失,双方产生误解,以为对方没有收到消息,陷入等待之中。如果此时没有设置 timeout,就会造成互相的等待一直持续下去,从而形成死锁。
(4)数据库锁:如果某个线程对数据库表或者行加锁,但是意外导致没能正确释放锁,而其他线程则会等待数据库锁的释放,从而陷入死锁。
(5)文件锁:某个线程获取文件锁后开始执行。但是执行过程中意外退出,而没能释放锁。那么其他等待该文件锁的线程将会一直等待,直到系统释放文件句柄的资源。
(6)死循环:假如某个线程,由于编码问题,在对资源加锁后,陷入死循环,导致一致无法释放锁。

监控工具

命令行输入 : jConsole
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值