多线程

synchronized

synchronized 用在方法签名上,当某个线程调用此方法时,会获取该实例的对象锁,方法未结束之前,其他线程只能去等待。当这个方法执行完时,才会释放对象锁。其他线程才有机会去抢占这把锁,去执行方法该方法,但是发生这一切的基础应当是所有线程使用的同一个对象实例,才能实现互斥的现象。否则synchronized关键字将失去意义。

synchronized 用在代码块的使用方式:synchronized(obj) { //todo code here }

当线程运行到该代码块内,就会拥有obj对象的对象锁,如果多个线程共享同一个Object对象,那么此时就会形成互斥!特别的,当obj == this时,表示当前调用该方法的实例对象。

使用synchronized 代码块相比方法有两点优势:

  1. 可以只对需要同步的使用

  2. 与wait()/notify()/nitifyAll()一起使用时,比较方便

wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。

sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。

wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!

notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用

notifyAll()则是唤醒所有等待的线程。

生产者消费者模型:两个线程依次打印"A""B",总共打印10次。

public class Consumer implements Runnable {

    @Override
    public void run() {
        int count = 10;
        while (count > 0) {
            synchronized (CPTest.obj) {
                System.out.print("B");
                count--;
                CPTest.obj.notify(); // 主动释放对象锁
                try {
                    CPTest.obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class Produce implements Runnable {

    @Override
    public void run() {
        int count = 10;
        while (count > 0) {
            synchronized (CPTest.obj) {
                System.out.print("A");
                count--;
                CPTest.obj.notify();
                try {
                    CPTest.obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class CPTest {
    public static final Object obj = new Object();

    public static void main(String[] args) {
        new Thread(new Produce()).start();
        new Thread(new Consumer()).start();
    }
}

Lock

除了wait()和notify()协作完成线程同步之外,使用Lock也可以完成同样的目的。

ReentrantLock 与synchronized有相同的并发性和内存语义,synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock()。

public class Consumer implements Runnable {

    private Lock lock;

    public Consumer(Lock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        int count = 10;
        while (count > 0) {
            try {
                lock.lock();
                count--;
                System.out.print("B");
            } finally {
                lock.unlock(); //主动释放锁
                try {
                    Thread.sleep(91L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class Producer implements Runnable {
    private Lock lock;

    public Producer(Lock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        int count = 10;
        while (count > 0) {
            try {
                lock.lock();
                count--;
                System.out.print("A");
            } finally {
                lock.unlock();
                try {
                    Thread.sleep(90L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class CPTest {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        Consumer consumer = new Consumer(lock);
        Producer producer = new Producer(lock);
        new Thread(producer).start();
        new Thread(consumer).start();


    }
}

使用建议:

在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。

synchronized 关键字和 volatile 关键字的区别

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块

  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。

以i++为例,这其实是一个复合操作,包括三步骤:

  • 读取i的值。
  • 对i加1。
  • 将i的值写回内存。 volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。

线程池

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 线程池、数据库连接池、Http 连接池等等

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

通过 ThreadPoolExecutor 的方式创建线程池

模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。

AQS

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

多线程

上一篇:qt编译过程


下一篇:计算机硬件冯诺依曼结构