目录太长了,体验不好,就看左边的目录吧,自动生成的目录有点水。。
多线程概要
什么是进程?
在冯诺依曼体系下,整个计算机设备分为,应用程序,操作系统,处理器(cpu),主存,I/O设备。应用程序在操作系统的调节下在处理器上进行合理的资源分配,而在其中一个运行起来的程序,就是进程。
cpu有一个概念,核心数和线程,核心为物理核心,线程为逻辑核心
而进程管理其实分为两步:
1.描述一个进程:使用结构体或类,把一个进程有哪些信息,表示出来。
2. 组织这些进程:使用一定的数据结构,把这些结构体/对象,放在一起。
进程的特点:
- PID :每个进程需要有一个唯一的身份标识
- 内存指针:当前这个进程使用的内存是哪一部分,进程一旦开启,就会消耗一定的硬件资源
- 文件描述符:进程每次打卡一个文件,就会产生一个“文件描述符” ,被标识了的意味着这个文件已打开。而一个进程会打开多个文件,然后呢就会把这些文件描述符放到循序表中,构成文件描述符表
- 进程调度:
- 进程状态:就绪态,阻塞态,前者表示该进程已准备好,可以随时上cpu上执行,后者还需等待
- 进程优先级:那个进程优先级高就先执行那个进程
- 进程的上下文:就是描述了当前进程执行到哪里这样的“存档记录”,进程在离开CPU的时候就要把当前运行的中间结果存档在cpu的寄存器中,等到下次进程回来CPU上,在恢复之前的存档,从上次结果开始
- 进程的记账信息:统计了每个进程,在CPU上执行了多久,可以作为调度的参考依据。
- 并发和并行:指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个进程在运行(处于running),并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象。
内存分配:
操作系统给进程分配的内存,是以“虚拟地址空间”的方式进行分配。
什么是多线程
之前说过,进程是一个运行程序,然而一个程序内的功能有很多个,而这其中就有一个问题,就是客户可能会同时用一个程序的多个功能。诺是按照以前我们的写发就是一个main方法,去实现一个主要功能,肯定是不行的。为了应对这个情况,多线程运行就在所难免。
需求决定技术发展
线程是更轻量的的进程。约定一个进程可以包含多个线程,此时多个线程每个线程都是一个独立可以调度执行的执行流(并发),这些线程公用同一份进程的系统资源。
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
可以理解为,一个工厂(进程),中有很多个生产线:(线程)(调用同一份资源,内存空间,文件描述符)。
其中几个问题要重点理解。一个厂子也就意味着资源和场地是一定的,如果为了生产效率,盲目去增加生产线,不去顾忌这些,反而会使的整个生成效率变慢。同理一个主机的核心也是有限,所以增加的线程数和进程数也是有限度。而一台主机到限度了,就可以增加另一台主机,从而使得核心数增加(也就是分布式处理)。
进程和线程的区别:
1.进程包含线程
2.进程有自己独立的内存空间和文件描述符,同一个进程的多个线程之间,共享同一份地址空间和文件描述符
3.进程是操作系统资源分配的基本单位,线程是操作系统调度的基本单位
4.进程之间具有独立性,一个进程挂了,不会影响到别的进程;同一个进程里的多个线程之间,一线程挂了,可能会把整个进程带走,会影响到其他线程的。
多线程编程:
这是我们第一个多线程。
public class ThreadDemo {
private static class MyThread extends Thread {
@Override
public void run() {
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
// 随机停止运行 0-9 秒
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
// 随机停止运行 0-9 秒
e.printStackTrace();
}
}
}
}
创建线程
1.继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
创建一个线程实例
MyThread t = new MyThread();
调用 start 方法启动线程
t.start(); // 线程开始运行
2.实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
Thread t = new Thread(new MyRunnable());
调用start方法
t.start(); // 线程开始运行
class MyRunnabble implements Runnable{
@Override
public void run() {
while (true)
{
System.out.println("123__true");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class test1 {
public static void main(String[] args) {
MyRunnabble runnabble=new MyRunnabble();
Thread t=new Thread(runnabble);
t.start();
while (true) {
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
- 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()
常见方法:
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
常见属性:
// 使用匿名类创建 Thread 子类对象Thread t1 = new Thread () {@Overridepublic void run () {System . out . println ( " 使用匿名类创建 Thread 子类对象 " );}};
// 使用匿名类创建 Runnable 子类对象Thread t2 = new Thread ( new Runnable () {@Overridepublic void run () {System . out . println ( " 使用匿名类创建 Runnable 子类对象 " );}});
// 使用 lambda 表达式创建 Runnable 子类对象Thread t4 = new Thread (() -> {System . out . println ( " 使用匿名类创建 Thread 子类对象 " );});
多线程的优势
public class test3 {
static int count=0;
public synchronized static void sum(){
count++;
}
public static void main(String[] args) throws InterruptedException {
long time=System.currentTimeMillis();
Thread t1=new Thread(()->{
for (int i = 0; i < 10000; i++) {
sum();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 10000; i++) {
sum();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
System.out.println(System.currentTimeMillis()-time);
}
}
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况,下面我们会进一步说明
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
- 线程的中断问题,下面我们进一步说明
中断问题:
1. 通过共享的标记来进行沟通
public class ThreadDemo {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ ": 转账");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 通知李四对方是个骗子!");
target.isQuit = true;
}
}
2. 调用 interrupt() 方法来通知
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种方法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 转账!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 开始转账。");
thread.start();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ ": 对方是个骗子!");
thread.interrupt();
}
}
方法 | 说明 |
public void interrupt() |
中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位
|
public static boolean
interrupted()
| 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean
isInterrupted()
| 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
- 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
- Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
- Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
等待一个线程-join()
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
public class test2 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(){
@Override
public void run() {
while (true) {
System.out.println("123545_run");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
t.join();
while (true) {
System.out.println("123545_run");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
NEW: 安排了工作 , 还未开始行动RUNNABLE: 可工作的 . 又可以分成正在工作中和即将开始工作 .BLOCKED: 这几个都表示排队等着其他事情WAITING: 这几个都表示排队等着其他事情TIMED_WAITING: 这几个都表示排队等着其他事情TERMINATED: 工作完成了 .
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
线程安全(风险)
public class test3 {
static int count=0;
public static void sum(){
count++;
}
public static void main(String[] args) throws InterruptedException {
long time= System.nanoTime();
Thread t1=new Thread(()->{
for (int i = 0; i < 10000; i++) {
sum();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 10000; i++) {
sum();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
我们会发现这个代码出现了一个问题,与我们想要的预期不一致。即出现bug
其本质是,count++操作,本质是有三个CPU指令构成
1.load,把内存中的数据读到cpu寄存器中。
2.add,就是把寄存器中的值,进行+1操作
3.save,把寄存器中值写回内存中。
大家肯定学过数学,那么对于组合,肯定是有过了解的。那么我问个问题,现在线程有两个,分别对count进行++操作。对于cpu来说有几种组合方式?3*3共有9种,那么问题来了,我们只要唯一的结果,不需要这么结果可能。
- 线程之间的共享变量存在 主内存 (Main Memory).
- 每一个线程都有自己的 "工作内存" (Working Memory) .
- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
![](https://img-blog.csdnimg.cn/ac9a9ea37d314676a162cc76353f6bcc.png)
其实在进一步理解,可以理解为,两个线程对同一个变量,进行了相互作用。
线程不安全的原因:
1.抢占式执行(大部分的原因)
2.多个线程修改同一个变量(不安全)(而有几种情况是安全:
一个线程改同一个变量(安全),多个线程读同一个变量(安全),多个线程修改不同变量)
3.修改操作,不是原子性的。
4.内存可见性,引起的线程不安全。
5.指令重排序,引起的线程不安全。
解决线程不安全:
synchronized
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
内存刷新
可重入
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
- increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
- 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)
可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息:
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增。
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)。
synchronized 使用:
- 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
- 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
- 修饰代码块: 明确指定锁哪个对象.
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer
volatile 关键字
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
根据上面的代码,发现问题没有,无论我们在控制台中输入什么值,程序都不会结束。为什么呢,就像上面所说的那样,一个cup的寄存器中数据并没进行更新。另一个线程所拿到的数据没有进行跟换。其主要原因是计算机运算速度太快了。寄存器和缓存的速度都太快了,
使用特点:
1.volatile 不保证原子性
2.volatile 适用于一个线程读,一个线程写
3.synchronized 既能保证原子性, 也能保证内存可见性.
改正后:
static class Counter {
volatile public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
此时就可以结束进程了。
wait 和 notify
- wait() / wait(long timeout): 让当前线程进入等待状态.
- notify() / notifyAll(): 唤醒在当前对象上等待的线程.
wait()
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁.
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
notify ()
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
- 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
- 完,也就是退出同步代码块之后才会释放对象锁。
wait和notify的使用(只能一个结束,一个开始)
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
wait与sleep的区别:
多线程模式:
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
加锁进行实例化对象,是很耗资源。其实一个实例创建后,在内存中已经存在,其他线程其实更多的是读操作。那么就没必要去进行加锁操作。
对上述代码改进:
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
//判断是否为空,诺不为空,就不需进行加锁实例化。
if (instance == null) {
synchronized (Singleton.class) {
//进行实例化判断
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
两个if所代表的意义有所不同。
1.加锁if:把if和new变为原子操作
2.双重 if:减少不必要的加锁操作
3.使用volatile 禁止指令重排序,保证后续线程肯定拿到的是完整对象。
单例模式:线程安全问题:
饿汉模式:天然就是安全的,只是读操作
懒汉模式:不安全的,有读也有写。
阻塞队列(线程安全)
本质是一个循环队列,但是它带有阻塞特性;
1.如果入队列为空,尝试出队列,就会阻塞等待。等待到队列不空为止。
2.如果队列满了,尝试入队,就会阻塞等待,等待到队列不满为止。
这个就有一个经典的模型进行解释--生产者消费者模型,什么是生产者消费者模型?其实简单理解为,生产效率与消费效率的比值。比如一个面包厂1小时生产4个面包,而此时有很多人等着吃面包。这个就是简单的生产者消费者模型。
这个数据结构的模式有几个好处:
1.可以让上下游模块之间,可以更好的“解耦和”。
队列与具体业务无关,队列中的某一个线程挂了,不影响其他线程,比如电脑有时候网页会卡,但是某些功能还在运行。
2.削峰填谷
不知道各位有没有打游戏,王者农药肯定都听过,其中有个事情,在游戏早期,它出了一款皮肤,这个皮肤很受玩家喜欢,再上线的那一刻,众多玩家,蹲点购买。使得当时的支付系统蹦了几分钟。为了应对这种情况,阻塞队列就可以减少这种风险。
在Java中提供了一个阻塞队列的数据的集合,BlockingQueue
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
重点是如何自己去实现这种数据结构:
主要分为三步:
1.先实现一个普通队列
2.加上线程安全
3.加上阻塞功能
class MyBlockingQueue{
//普通队列
private int [] items=new int[1000];
//规定head--tail的范围为有效范围
volatile private int head=0;
volatile private int tail=0;
volatile private int size=0;
//入队列
synchronized public void put(int elem) throws InterruptedException {
//队列元素满了
while (size==items.length){
this.wait();
}
items[tail]=elem;
tail++;
//判断是否到达末尾,队列中的元素没有满的情况下
if (tail==items.length){
tail=0;
}
//可读性下差,开发效率慢
//tail=tail%items.length;
this.notify();
size++;
}
//出队列
synchronized public Integer take() throws InterruptedException {
while (size==0){
this.wait();
}
int value=items[head];
head++;
if (head==items.length){
head=0;
}
this.notify();
size--;
return value;
}
}
public class test8 {
public static void main(String[] args) {
MyBlockingQueue queue=new MyBlockingQueue();
Thread t1=new Thread(()->{
while (true){
try {
int value= queue.take();
System.out.println("消费:"+value);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2=new Thread(()->{
int value=0;
while (true) {
try {
System.out.println("生产:"+value);
queue.put(value);
Thread.sleep(1000);
value++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
}
入队成功后其他线程才能出队,出队成功后其他线程才能入队。
定时器:
在java中提供了Timer类。Timer 类的核心方法为 schedule ,其中包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒)。
Timer timer = new Timer ();timer . schedule ( new TimerTask () {@Overridepublic void run () {System . out . println ( "hello" );}}, 3000 );
实现一个定时器:
- 一个带优先级的阻塞队列(阻塞队列中的任务都有各自的执行时刻 delay. 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.)
- 队列中的每个元素是一个 Task 对象.
- Task 中带有一个时间属性, 队首元素就是即将运行的。
- 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
class MyTask implements Comparable<MyTask>{
public Runnable runnable;
//为了方便后续,使用绝对的时间戳
public long time;
public MyTask(Runnable runnable,long delay){
this.runnable=runnable;
//获取当前时刻的时间戳+delay,作为任务的实际执行时间
this.time=System.currentTimeMillis()+delay;
}
@Override
public int compareTo(MyTask o) {
//设置比较器,构建优先级队列
return (int)(this.time-o.time);
}
}
class MyTimer{
//这个结构,带有优先级的阻塞对列,核心数据结构
//创建一个锁对象
private Object loker=new Object();
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//此处的dalay 是一个形如3000这样的数字(多长时间后执行)
public void schedule(Runnable runnable,long dalay){
//根据参数,构造MyTask,插入队列即可
MyTask myTask=new MyTask(runnable,dalay);
queue.put(myTask);
synchronized (loker){
loker.notify();
}
}
//构造线程
public MyTimer(){
Thread t=new Thread(()->{
while (true) {
try {
synchronized (loker){
MyTask myTask=queue.take();
long curTime=System.currentTimeMillis();
if (myTask.time <= curTime){
//时间到了,执行任务
myTask.runnable.run();
}else {
//时间还没到
//将刚刚取出的任务,重新塞回队列
queue.put(myTask);
loker.wait(myTask.time-curTime);
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
public class test10 {
}
线程池:
(就是就是装有很多线程的仓库,使用线程从里面拿就好)
ExecutorService pool = Executors . newFixedThreadPool ( 10 );pool . submit ( new Runnable () {@Overridepublic void run () {System . out . println ( "hello" );}});
![](https://img-blog.csdnimg.cn/aa920af0ceb74e428f8b3476692c7446.png)
- corePoolSize:核心线程数(不会消失)
- maximumPoolSize:最大线程数(核心线程数+零时线程)
- keepAliveTime: 临时线程允许的空闲时间.
- unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
- workQueue: 传递任务的阻塞队列
- threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
- RejectedExecutionHandler: 拒绝策略, 如果任务量超出线程池的负荷了接下来怎么处理:
- AbortPolicy(): 超过负荷, 直接抛出异常.
- CallerRunsPolicy(): 调用者负责处理
- DiscardOldestPolicy(): 丢弃队列中最老的任务.
- DiscardPolicy(): 丢弃新来的任务
- newFixedThreadPool: 创建固定线程数的线程池
- newCachedThreadPool: 创建线程数目动态增长的线程池.
- newSingleThreadExecutor: 创建只包含单个线程的线程池.
- newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的Timer.
实现线程池
- 核心操作为 submit, 将任务加入线程池中
- 使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
- 使用一个 阻塞队列中组织所有的任务
- 每个 worker 线程要做的事情: 不停的从 阻塞队列中取任务并执行.
- 指定一下线程池中的最大线程数 maxWorkerCount,当前线程数超过这个最大值时, 就不再新增线程了。
class Worker extends Thread {
private LinkedBlockingQueue<Runnable> queue = null;
public Worker(LinkedBlockingQueue<Runnable> queue) {
super("worker");
this.queue = queue;
}
@Override
public void run() {
// try 必须放在 while 外头, 或者 while 里头应该影响不大
try {
while (!Thread.interrupted()) {
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
}
}
}
public class MyThreadPool {
private int maxWorkerCount = 10;
private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue();
public void submit(Runnable command) {
if (queue.size() < maxWorkerCount) {
// 当前 worker 数不足, 就继续创建 worker
Worker worker = new Worker(queue);
worker.start();
}
// 将任务添加到任务队列中
queue.put(command);
}
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool();
myThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("吃饭");
}
});
Thread.sleep(1000);
}
}
锁策略
上面我说过,锁是为了解决线程冲突的问题。但是我也说过加锁操作会影响程序的效率。(因为阻塞),为了应对这个我们应该合理去进行加锁操作,那么就应该有策略的操作。
1.乐观锁VS悲观锁
乐观锁: 预测接下来冲突概率不大(做的工作少)--->效率会快一些
悲观锁:预测接下了的冲突概率不大(做的多)--->x效率会慢一些
其实这两个就是预测接下来的锁冲突(阻塞等待)的概率是大,还是不大,根据这个冲突的概率,决定接下来怎么做。
2.轻量级锁VS重量级锁
轻量级锁:加锁解锁的过程更快更高效。(一个乐观锁很可能是一个轻量级锁)
重量级锁:加锁解锁,过程更慢,更低效。(一个悲观锁很可能是一个重量级锁)
3.自旋锁VS挂起等待锁
自旋锁:是轻量级锁的一种典型实现(纯用户态的不需要经过内核态(时间相对更短))
加锁失败后,不停等待的去问是否可以加锁了
挂起等待锁:是重量级锁的一种典型实现(通过内核机制来实现挂起等待(时间更长了))
加锁失败后,先去做其他事情,等这个锁给我信号后我就回来加锁。
Synchronized 既是悲观锁,也是乐观锁,既是轻量级锁,也是重量级锁;轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。
而Synchronized 会根据当前锁竞争的激烈程度,自适应;
- 如果冲突不激烈,以轻量级锁或者乐观锁的状态运行
- 如果激烈,以重量级锁或悲观锁的状态运行。
4.互斥锁VS读写锁
互斥锁:
synchronized是一个互斥锁,就单纯的加锁。通常只有两种操作:
- 进入代码块,加锁
- 出代码块,解锁
读写锁:
有一种锁,把读操作和写操作分开加锁(线程安全):
- 给读加锁
- 给写加锁
- 解锁
约定:
- 读锁和读锁之间,不会锁竞争,不会产生冲突(不会影响程序之间的效率)
- 写锁和写锁之间,有锁竞争(减慢速度,保证准确性)
- 读锁和写锁之间,有锁竞争(减慢速度,保证准确性)
Java中专门提供了读锁一个类,写锁一个类。
5.可重入锁vs不可重入锁
- 如果一个锁,在一个线程中,连续对锁,锁了两次,不产生死锁,叫可重入锁。
- 如果一个锁,在一个线程中,连续对锁,锁了两次,产生死锁,叫不可重入锁。
死锁的第一种情况
如何产生死锁,我们对一个代码加两次锁,此时内部的锁要等待外部的锁释放才能加锁,而此时外部的锁释放,需要等待内部锁加锁成功。然后逻辑上矛盾了,于是产生了死锁。
死锁的第二种情况
两个线程两把锁,即使单个线程是可重入锁,也会死锁。
线程1的外部锁加锁,需要等待线程2内部锁释放,同理线程2外部锁加锁,需要等待线程1内部锁释放,此时逻辑矛盾,产生死锁。
死锁的第三种情况
哲学家,就餐问题(N个线程,M把锁)
一个桌子上有五只筷子。也有五个人,桌上有一碗面,每个人只能用一双筷子吃一口。诺是五个同时拿起一只筷子,场上就构不成一双筷子的条件,也就是谁都吃不了面。此时就死锁了。
怎么办,很简单,五个人约定一个规则,谁先吃,谁后吃,此时就可以避开死锁的情况。
死锁的四个必要条件
- 互斥使用:一个线程拿到一把锁后,另一个线程不能使用(根本问题锁的基本特点)
- 不可抢占:一个线程拿到锁,只能自己主动释放,不能是被其他线程强行占有
- 请求和保持:一个线程拿到一个锁,不去做事,反而想拿到第二把锁。
- 循环等待:逻辑冲突。谁都拿不到。
实践中如何避免死锁?
对锁进行编号,如果需要获取多把锁,就约定加锁顺序,务必先对编号小的加锁,在对编号大的加锁。
公平锁VS非公平锁
约定:
遵循先来后到,就是公平锁,不遵守先来后到的(等概率竞争是不公平的),非公平锁。
synchronized是非公平的,要实现公平就需要在synchronized的基础上,加个队列来记录这些加锁线程的顺序。
总结一下synchronized的特点:
- 既是乐观锁,也是悲观锁
- 既是轻量级锁,也是重量级锁
- 轻量级锁基于自旋锁实现,重量级锁基于挂起等待实现
- 不是读写锁
- 是可重入锁
- 是非公平锁
CAS
![](https://img-blog.csdnimg.cn/19a0b5168e6d4e198221088e4643a3ea.png)
1.实现原子类
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
2.实现自旋锁
但是CAS不是没有问题,最典型的问题A->B->A问题,其实就是我们要内存改变的值与内存的值一样,是得不断在A--B--A中不断横跳。在具体一点就是,两个线程(t1,t2)对数据进行减法,(t3)还有一个对数据进行加法,而加的数据与减的数据一样。
那么就会有一个问题。两个线程中其中一个线程(t1)提前做了减操作,接下来是(t3)加操作,此时内存的值没变,t2线程发现值是原来的值,又做了一次减操作。(这显然不是我们所期望的)
如何解决呢?
加入一个衡量内存的值是否变化的量,俗称版本号,版本号只能增加无法减少,每一次修改版本+1,这样我只需对比版本号本身就可以避免aba问题。
synchronized的锁策略:锁升级
偏向锁:非必要,不加锁
先让线程针对锁,有个标记,如果整个代码执行过程中没有遇到别的线程和我竞争这个所,我就加锁了。但是如果有人来竞争,就升级为真的锁。这样既保证了效率,也保证了线程安全。
锁消除
基础逻辑是,非必要不加锁。编译器+JVM 判断锁是否可消除,如果可以,就直接消除。检测当前代码是否多线程执行,判断是否有必要加锁,如果没有必要,但是又加上了锁,就会在编译过程中自动取消掉。
比如StringBuffer,在源码内加入了synchronized关键字。诺是单线程就必要加锁了,也就可以取消掉。
锁粗化
锁的粒度,synchronized代码块,包含代码的多少(代码越多,粒度越粗,越少,粒度越细),多数情况希望锁的粒度更小。(串行代码少,意味着并发代码就多。)
如果有一个场景需要频繁的加锁解锁,此时就会将整个场景锁起来,变成一个更粗的锁
Callable 的用法
//创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象
static class Result {
public int sum = 0;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread() {
@Override
public void run() {
//main 方法中先创建 Result 实例, 然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + ... + 1000
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
synchronized (result.lock) {
result.sum = sum;
//主线程同时使用 wait 等待线程 t 计算结束
result.lock.notify();
}
}
};
t.start();
synchronized (result.lock) {
//
while (result.sum == 0) {
result.lock.wait();
}
//当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果.
System.out.println(result.sum);
}
}
创建线程计算 1 + 2 + 3 + ... + 1000(callable)
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
- 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
- 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
- 把 callable 实例使用 FutureTask 包装一下.
- 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的
- call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
- 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结 果.
Callable中泛型是什么,就返回什么。
Callable 和 Runnable的区别
- Callable 和 Runnable 相对, 都是描述一个 "任务",Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务。
- Callable 通常需要搭配 FutureTask 来使用.,FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定。
- FutureTask 就可以负责这个等待结果出来的工作。
FutureTask 的理解,其实可以理解为,炖汤,通常炖汤我们将食物放入砂锅中,只需要等待时间过去2-3小时,砂锅就能为我们呈现一锅鲜美的汤。
JUC(ava.util.concurrent)
- lock(): 加锁, 如果获取不到锁就死等.
- trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.(加锁失败,不会阻塞,直接返回false,更灵活)
- unlock(): 解锁
- synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准 库的一个类, 在 JVM 外实现的(基于 Java 实现).
- synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就 放弃.
- synchronized 是非公平锁, ReentrantLock 默认是非公平锁,但是提供了公平和非公平两种工作模式. 可以通过构造方法传入一个 true 开启公平锁模式.
-
更强大的唤醒机制 . synchronized 是通过 Object 的 wait / notify 实现等待 - 唤醒 . 每次唤醒的是一 个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待 - 唤醒 , 可以更精确控制唤醒某个指定的线程.
- 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便。
- 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等。
- 如果需要使用公平锁, 使用 ReentrantLock。
原子类
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
信号量 Semaphore
本质是一个计数器,描述了当前“可用资源”的个数
- P操作,申请资源。计数器-1;
- V操作,释放资源。计数器+1;
如果计数器为0,就阻塞等待,等待出现资源时,及继续申请等待。
- 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源。
- acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
- 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果。
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire();
System.out.println("我获取到资源了");
Thread.sleep(1000);
System.out.println("我释放资源了");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable);
t.start();
}
CountDownLatch
- 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成。
- 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减。
- 主线程中使用 latch.await(); (暗中计算有几个countDown被调用了)阻塞等待所有任务执行完毕. 相当于计数器为 0 了。
public class Demo {
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10);
Runnable r = new Runable() {
@Override
public void run() {
try {
Thread.sleep(Math.random() * 10000);
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i++) {
new Thread(r).start();
}
// 必须等到 10 人全部回来
latch.await();
System.out.println("比赛结束");
}
}
多线程对集合类的使用
常用的集合类:ArrayList,LinkedList,HashMap,PriorityQueue。。。线程是不安全的。
如果要使用怎么办?
1.可以手动对集合的修改操作加锁。(synchronized 或者 ReentrantLock)
2.使用java标准库提供的一些线程安全的版本的集合类。
多线程环境使用 顺序表
- synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
- synchronizedList 的关键操作上都带有 synchronized
- 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy, 复制出一个新的容器,然后新的容器里添加元素,
- 添加完元素之后,再将原容器的引用指向新的容器。
- 在读多写少的场景下, 性能很高, 不需要加锁竞争.
- 占用内存较多.
- 新写的数据不能被第一时间读取到.
多线程环境使用队列
- ArrayBlockingQueue 基于数组实现的阻塞队列
- LinkedBlockingQueue 基于链表实现的阻塞队列
- PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
- TransferQueue 最多只包含一个元素的阻塞队列
多线程环境使用哈希表
- Hashtable
是线程安全的,给关键方法加上synchronized,颗粒度比较粗。它对整个哈市表加锁,任何的增删查操作,都会触发加锁,也就意味着会有锁竞争。其实没有必要,哈希表是有桶的,修改值是要通过key计算hash值,然后将新元素放到链表上。
两个线程对不同量进行修改,不会产生冲突,但是由于方法上加了锁也就意味着,两个线程同时使用一个方法会阻塞。(所以不建议)
- ConcurrentHashMap
其他方面的改进:
更充分的利用了CAS机制--无锁编程
优化了扩容策略
- HashMap: 线程不安全. key 允许为 null
- Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
- ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null
- JVM 把内存分成了这几个区域:
- 方法区, 堆区, 栈区, 程序计数器.
- 其中堆区这个内存区域是多个线程之间共享的.
- 只要把某个数据放到堆内存中, 就可以让多个线程都能访问到。
- 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
- 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.