多线程学习笔记

廖神学习地址

一、多线程基础

1、进程

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

2、进程 vs 线程

进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。
具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:
创建进程比创建线程开销大,尤其是在Windows系统上;
进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

3、多线程

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

因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java多线程编程的特点又在于:

多线程模型是Java程序最基本的并发模型;
后续读写网络、数据库、Web开发等都依赖Java多线程模型。

二、创建新线程

方法一:从Thread派生一个自定义类,然后覆写run()方法:

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实例时,传入一个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!");
    }
}

当然也可以使用lambda表达式这里就不做演示。
注意:
1、sleep()传入的参数是毫秒。调整暂停时间的大小,我们可以看到main线程和t线程执行的先后顺序。
2、直接调用run方法是无效的,和普通的调用没什么区别,只有调用start方法才会创建线程。
3、线程的优先级:

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

三、线程的状态

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

线程种植的原因:

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

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过**t.join()**等待t线程结束后再继续运行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello");
        });
        System.out.println("start");
        t.start();
        t.join();
        System.out.println("end");
    }
}

四、中断线程

1、interrupt()方法

对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。
如果目标线程处于等待状态,该线程会捕获到InterruptedException;
目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;

2、volatile关键字

在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
就会导致,a线程改变了变量,只是把这个变量写到副本中,这样b线程读取的话读取不是最新的,这样多线程变量不一致出现错误。

因此,volatile关键字的目的是告诉虚拟机:
每次访问变量时,总是获取主内存的最新值;
每次修改变量后,立刻回写到主内存。
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

五、守护线程

在一般情况下,线程执行完毕之后jvm虚拟机也会退出,进程结束。但是会有一些线程是无限循环的,无法结束,所以jvm也不会退出,造成资源一直浪费。这时就需要守护线程了。
守护线程:
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

六、线程同步

1、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 int count = 0;
}

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

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

代码很简单,两个线程执行 1w次加减操作,最后结果应该等于0.
但是执行发现每次结果都不一样。
这时我们需要加锁操作:

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

这样最终结果就会为 0
把Counter.lock示意为一个锁,当A线程执行完之后会释放锁,在A线程未执行完时候,B线程获取不到这个锁也就不会执行,等待A释放锁之后B自己获得锁开始执行该线程。
概括一下:

  1. 找出修改共享变量的线程代码块;
  2. 选择一个共享实例作为
  3. 使用synchronized(lockObject) { … }。
  4. 用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this;

当然synchronized也可以修饰方法:

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

上述代码等价于

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

当修饰静态变量的时候,静态变量是针对于类的,所以修饰静态变量等价于修饰该类。

public synchronized static void test(int n) {
    ...
}
//这两个是等价的
public class Counter {
    public static void test(int n) {
        synchronized(Counter.class) {
            ...
        }
    }
}

synchronized关键字加锁机制,会降低执行效率,应该选择使用。

不需要synchronized的操作,JVM规范定义了几种原子操作:

  1. 基本类型(long和double除外)赋值,例如:int n = m;
  2. 引用类型赋值,例如:List list = anotherList。
  3. 当一个方法有两个复制语句的话则需要同步
//这时是不安全的 需要同步操作
public void setTest(int a,int b){
	this.bud=a;
	this.aud=b;
}

long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。

七、死锁

先看一段代码

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

如果有两个线程A、B分别调用add方法和dec方法,
A线程首先获得lockA锁,B线程获得LockB锁,接着A线程想要去获得lockB锁就需要等待B线程释放,而B线程需要获得lockA锁,需要等待A线程释放,所以会无限循环下去,这就是死锁。
那应该正确的获取呢:
线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()

public void dec(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value -= m;
        synchronized(lockB) { // 获得lockB的锁
            this.another -= m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

八、wait和notifyAll和notify

先看一个例子

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

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

    public synchronized String getTask() {
        while (queue.isEmpty()) {
        }
        return queue.remove();
    }
}

乍一看代码好像没什么问题,当有A B两个线程去调用addTask和getTask方法时,当B线程调用时,会一直处于循环中不会释放锁,所以A线程永远不会去执行。
我们想要的效果是:

  1. A线程可以往里添加。
  2. B线程可以清除。
    所以我们应该这样做,当队列为空时让其处于等待状态,等A线程添加进去之后唤醒B然后去清除。
    代码稍作修改:
class TaskQueue {
    Queue<String> queue = new LinkedList<>();

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

    public synchronized String getTask() {
        while (queue.isEmpty()) {
        this.wait();
        }
        return queue.remove();
    }
}

总结:

  1. wait和notify用于多线程协调运行:
  2. 在synchronized内部可以调用wait()使线程进入等待状态;
  3. 必须在已获得的锁对象上调用wait()方法;
  4. 在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;
  5. 必须在已获得的锁对象上调用notify()或notifyAll()方法;
  6. 已唤醒的线程还需要重新获得锁后才能继续执行。

九、ReentrantLock

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

因为synchronized是Java语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLock是Java代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁。

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

和synchronized不同的是,ReentrantLock可以尝试获取锁:

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

上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。

所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。

十、Condition

class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

我们需要 lock调用newCondition实例一个Condition对象。
Condition提供的**await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()**是一致的,并且其行为也是一样的:

  1. await()会释放当前锁,进入等待状态;
  2. signal()会唤醒某个等待线程;
  3. signalAll()会唤醒所有等待线程;
  4. 唤醒线程从await()返回后需要重新获得锁。
    此外。condition还可以尝试等待时间,如果超过等待时间没有被唤醒,则自动唤醒。
if (condition.await(1, TimeUnit.SECOND)) {
    // 被其他线程唤醒
} else {
    // 指定时间内没有被其他线程唤醒
}

十一、ReentrantLock

public class Counter {
    private final Lock lock = new ReentrantLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        lock.lock();
        try {
            counts[index] += 1;
        } finally {
            lock.unlock();
        }
    }

    public int[] get() {
        lock.lock();
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            lock.unlock();
        }
    }
}

当线程调用inc()方法的时候,应该设置锁,但是当我们读取的时候调用get()方法的时候是可以不需要锁的。那我们应该怎么满足这个要求呢。
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(); // 释放读锁
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值