栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > Java

多线程 (进阶+初阶)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

多线程 (进阶+初阶)

文章目录
  • 一、进程和线程是什么
    • 1.1程序
    • 1.2端口号和PID
    • 1.3进程和线程
      • 有进程实现并发编程为什么还要使用线程?
      • 两者区别(面试重点)
    • 1.4串行、并行、并发
  • 二、Java.lang.Thread
    • 2.1第一个多线程代码
      • 问题1
      • 问题2
    • 2.2 jconsole命令
    • 2.3创建线程的四种方法(重点)
      • 继承Thread类
      • 实现Runnable接口
      • 两种创健线程方式的不同写法
        • 匿名内部类
        • Lambda表达式
    • 2.4并发和串行时间对比
      • 练习
      • 面试小问题
  • 三、Thread常用方法
    • 3.1构造方法
    • 3.2Thread类的核心属性
    • 3.3中断线程的两种方法
    • 3.4线程中断通知(重点)
    • 3.5等待另一个线程
      • join()方法延伸
    • 3.6获取当前线程对象和休眠当前线程对象
    • 3.7线程的状态
      • NEW 和 RUNNABLE状态
      • 三种阻塞状态
      • yield()方法
      • 线程状态意义
  • 四、线程安全
    • 4.1线程不安全概念
    • 4.2Java的内存模型-JMM
    • 4.3线程安全的三大特性
      • 原子性
      • 可见性
      • 关于内存的问题
      • 指令重排
      • 总结(面试重点)
    • 4.4 synchronize关键字
      • monitor lock对象锁
        • mutex lock互斥(synchronize第一个特性)
        • 代码块刷新内存(第二个特性)
        • 上锁操作和单线程操作区别
        • 可重入(第三个特性,面试重点)
    • 4.5 synchronized修饰类中的成员方法
    • 4.6 synchronized修饰类中的静态方法
    • 4.7 synchronized修饰当前的对象(难点)
    • 4.8 synchronized修饰当前class对象(难点)
    • 4.9线程安全类(了解即可)
  • 五、volatile关键字
    • 5.1保证可见性
    • 5.2 内存屏障
      • volatile保证可见性为什么不保证原子性?
      • volatile和sleep区别
  • 六、线程间等待与唤醒机制
    • 6.1 wait()等待
    • 6.2 notify()唤醒
    • 6.3等待队列 阻塞队列
        • 面试题:sleep()和wait()的区别
  • 七、单例模式
    • 7.1饿汉式单例
    • 7.2懒汉式单例
    • 7.3饿汉和懒汉的线程安全问题
      • 解决懒汉式线程安全问题
  • 八、阻塞队列
    • 8.1生产消费者模型
    • 8.2JDK中阻塞队列BlockingQueue
    • 8.3定时器常用方法
  • 九、线程池
    • 9.1线程池的作用
    • 9.2JDK线程池使用
    • 9.3Executors线程池工具类
    • 9.4ThreadPoolExector核心参数
      • 单线程池的意义
    • 9.5线程池工作流程
  • 十、常用锁的策略
    • 10.1悲观锁 乐观锁
    • 10.2乐观锁常用实现
    • 10.3读写锁
    • 10.4重量级锁和轻量级锁
    • 10.5公平锁和非公平锁

一、进程和线程是什么 1.1程序


1.2端口号和PID

1.3进程和线程


有进程实现并发编程为什么还要使用线程?

虽然多进程也能实现并发编程, 但是线程比进程更轻量.
创建线程比创建进程更快.
销毁线程比销毁进程更快.
调度线程比调度进程更快.

两者区别(面试重点)



Tset就是进程,main是主线程,此时多余的一个进程java.exe是idea自己

总结:

1.4串行、并行、并发

二、Java.lang.Thread 2.1第一个多线程代码

注意事项:
1.启动线程是start方法,不是run方法。通过start方法将线程启动以后,每个线程自动执行自己的run方法
2.Thread-0这些线程名字是默认的,可以修改
3.这四个线程同时执行,互不影响

问题1

主方法明明是在3个线程之后写的,并且每个线程run方法是死循环,为什么main方法是第一个打印的?
答:因为是并发执行,每个线程的执行互不影响,才造成这样的执行效果

问题2


main方法会打印,但是需要等m1m2m3方法调用结束才可以,但是m1的run方法是个死循环,所以只会一直打印m1的线程名字

2.2 jconsole命令

2.3创建线程的四种方法(重点) 继承Thread类

这个ThreadMethod我们自己继承的类就是一个线程实体,注意区分和runnable接口实现线程的区别

// 定义一个Thread类,相当于一个线程的模板
class MyThread01 extends Thread {
    // 重写run方法// run方法描述的是线程要执行的具体任务
    @Overridepublic
     void run() {
        System.out.println("hello, thread.");
    }
}
 

public class Thread_demo01 {
    public static void main(String[] args) {
        // 实例化一个线程对象
        MyThread01 t = new MyThread01();
        // 真正的去申请系统线程,参与CPU调度
        t.start();
    }
}


实现Runnable接口

这个实现runnable接口的子类,并不是直接的线程对象,只是线程的一个核心工作任务,这是一个线程任务和实体的关系

// 创建一个Runnable的实现类,并实现run方法
// Runnable主要描述的是线程的任务
class MyRunnable01 implements Runnable {
    @Overridepublic void run() {
        System.out.println("hello, thread.");
    }
}

public class Thread_demo02 {
    public static void main(String[] args) {
        // 实例化Runnable对象
        MyRunnable01 runnable01 = new MyRunnable01();
        // 实例化线程对象并绑定任务
        Thread t = new Thread(runnable01);
        // 真正的去申请系统线程参与CPU调度
        t.start();
    }
}

首先要先创建线程任务对象,才能创建线程对象,并且在创建线程对象时将线程任务对象传入进去。

两种创健线程方式的不同写法 匿名内部类
public class Thread_demo03 {
public static void main(String[] args) {
        Thread t = new Thread(){
            // 指定线程任务
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };
        // 真正的申请系统线程参与CPU调度
        t.start();
    }
}

public class Thread_demo04 {
public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            // 指定线程的任务
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());                
            }
        });
        // 申请系统线程参与CPU调度
        t.start();
    }
}

Lambda表达式
public class Thread_demo05 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            // 指定任务:任务是循环打印当前线程名
            while (true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    // 休眠1000ms
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 申请系统线程参与CPU调度
        t.start();
    }
}

2.4并发和串行时间对比

线程的创建和销毁也会耗时,所以实际的时间比理论实践多一点

练习


正解:可能先打印1也可能先打印2,具体先调度子线程输出还是先调度主线程输出由系统决定。
至于为什么多次试验都是 21的结果:


面试小问题

如果一个线程连续使用start()方法启动会怎么样?

三、Thread常用方法 3.1构造方法


注意事项:第一种方法一般搭配子类使用,因为Thread类中的run方法为空,什么都没有

3.2Thread类的核心属性


3.3中断线程的两种方法

注意Thread.sleep是个静态方法,在哪个线程用就在哪个线程生效


这里线程被终止的原因是因为加了break语句,否则不加break语句线程会改变状态继续执行

3.4线程中断通知(重点)


下图中使用的是类方法判断线程是否中断:Thread.interrupted()
它的作用是当线程被中断时threa.interrupt()之后,将中断状态ture置为false

下图中使用的是对象方法判断线程是否中断:Thread.currentThread().interrupted()
它的作用是当线程被中断时threa.interrupt()之后,不改变中断状态,仅仅只是查看当前线程是否中断,不做修改

红色方框查看线程是否为中断状态
橙色方框是修改线程为中断状态

3.5等待另一个线程


加了join()方法相当于图中t1,t2,主线程三个线程变成了串行,而不是并行

join()方法延伸

t1.start();
        // 主线程死等t1,直到t1执行结束主线程再恢复执行
        t1.join();
        // 此时走到这里,t1线程已经执行结束,再启动t2线程
        t2.start();
        // 在哪调用别的线程的join方法,阻塞的是调用join的线程
        // main -> 调用t2.join() 阻塞主线程,直到t2完全执行结束再恢复主线程的执行
        // 主线程只等t2 2000ms - 2s,若t2在2s之内还没结束,主线程就会恢复执行
        t2.join(2000);
        // t2线程也执行结束了,继续执行主线程
        System.out.println("开始学习JavaEE");
        System.out.println(Thread.currentThread().getName());
    }
}
3.6获取当前线程对象和休眠当前线程对象

3.7线程的状态


New状态到Runnable状态只需要start()方法
Runnable就两个状态:一个Ready和Running
超时等待时间到了就会还原状态:还原成ready状态

NEW 和 RUNNABLE状态


三种阻塞状态


yield()方法

就绪态:当前状态随时可能被CPU调度的状态

public class YieldTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
                // 春鹏线程就会让出CPU,进入就绪态,等待被CPU继续调度
                Thread.yield();
            }
        },"春鹏线程");
        t1.start();
        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
            }
        },"云鹏线程");
        t2.start();
    }
}

线程状态意义

四、线程安全 4.1线程不安全概念

比如t1和t2各自两个线程的run方法是累加5w的数值,那么最终两个线程应该相加得到10w,但是图中并发执行t1和t2线程却每次得到结果都不同。


4.2Java的内存模型-JMM


线程的局部变量:例如方法中的变量

4.3线程安全的三大特性 原子性

a+=10对应的三个原子性操作:先读取a的值,再执行a+10,最后将结果赋值给a

可见性


count++的原子性操作有三步,所以可能一个线程中还没进行完三步的操作,另一个线程就去读取count的值,可能导致每次线程读取的count值不是执行count++后正确的数值

举个例子:这个例子演示的是不可见性,但不一定这个数值一定是图中所说的情况造成的,只是其中的一种可能性。



第二种可能性:有很多种可能性,这里只是列举一种,总之最后的答案一定不是10w。

相当于本来计划+2次,但最终只+了1次,本来t1已经将count=1写回了主内存,但是t2也让count=1写回了主内存,最后count的值还是1,应该是2才对。


类似火车的售票系统:
客户A在买票时,发现还有一张票,当他下订单时,在主内存中nums=1-1=0没票了,但此时恰好客户B和A是同时准备购买票,但是此时nums=0还没有写回主内存,导致B也买了这张票(超卖现象)。

关于内存的问题


指令重排


总结(面试重点)

4.4 synchronize关键字

针对同一个对象才会导致线程安全问题,这里t1和t2处理的两个不同对象,最后结果都是5w

monitor lock对象锁

mutex lock互斥(synchronize第一个特性)




不管几个线程对这个对象进行处理,同一时刻只有一个线程拿到这个对象的锁,拿到这个锁就会执行increase()方法,另外线程就处于等待阻塞状态,哪个线程先拿到对象的锁不确定。

关于互斥的深入理解:到底有没有互斥关系,就看这些线程锁的是不是同一个对象


代码块刷新内存(第二个特性)

上锁操作和单线程操作区别

比如ABC三个同学都想上D老师的课,但是D老师每次只能给一个学生上课(这个上课就相当于一个方法),这就是单线程。但是ABC三个同学能同时获取D老师的VX(这也是一个方法)就可以多线程执行这个方法。

可重入(第三个特性,面试重点)


可重入的概念:

关于为什么加两次锁的原因:在increase1同步代码块里再加锁不是人为决定的

至于加不加synchronized的问题:

4.5 synchronized修饰类中的成员方法



4.6 synchronized修饰类中的静态方法



4.7 synchronized修饰当前的对象(难点)

相当于多线程优化,只把一个方法中部分代码锁住,执行时间就可以缩短

当不同的对象调用increase4()方法就不构成互斥了

4.8 synchronized修饰当前class对象(难点)


之前我们说的不构成互斥现象是因为各个线程调用的是不同对象里的同一个方法,现在构成互斥是因为虽然也是不同的对象调用这个该方法,但是synchronized锁的class是对象,代表每次只能有一个对象执行这个方法,因此,不管是几个对象,每个对象之间都构成互斥。

4.9线程安全类(了解即可)




五、volatile关键字 5.1保证可见性


如果flag之前不加volatile,当启动t1和t2线程以后,t1线程是一个死循环,t2线程可以输入数值改变flag的值使t1线程终止,但是当我们输入之后,t1还是没有终止。原因就是t1线程一直读取的是自己工作内存中的falg=0这个数值,没有读取到t2对flag更新的数值。但是加了volatile就会立刻退出循环,这就是保证了共享变量的可见性

注意事项:sychronized也能保证可见性但两者不等同

5.2 内存屏障

cpu在不影响结果的情况下对指令进行重排

volatile保证可见性为什么不保证原子性?

原子性在cpu执行的过程中一个或多个操作不能被其它线程中断。例如int a赋值操作是cpu天然的原子性,要让天生不原子性的操作

volatile和sleep区别

六、线程间等待与唤醒机制

6.1 wait()等待


6.2 notify()唤醒

随机唤醒:不一定先等待的就先唤醒



调用notify方法唤醒在等待的线程,一定要等notify方法中同步代码块里执行完毕才可以执行被唤醒的线程。

6.3等待队列 阻塞队列


每一个对象都有一个等待队列和阻塞队列,例如图中t3先获取到锁,所以t1和t2首先进入阻塞队列,t3调用wait方法就释放锁,t1和t2就去竞争锁,t3就会进入等待队列。当t1 t2 t3都进入等待队列,调用notifyall方法,则三个线程都被唤醒,但不是立即调度,从等待队列同时将三个队列置入阻塞队列去竞争锁

面试题:sleep()和wait()的区别

七、单例模式

7.1饿汉式单例

不管外部调用不掉用这个对象,只要类加载到JVM,唯一对象就会产生




7.2懒汉式单例

7.3饿汉和懒汉的线程安全问题

饿汉式是天然的线程安全,每个线程调用的都是同一个对象,但是懒汉式就不一样,因为每个线程开始时看到的内部对象都为空,可能每个线程都会产生一个对象:

解决懒汉式线程安全问题

1.直接在方法上加锁

2.优化 double - check
下图的写法不可取,因为其它线程也会卡在相同的位置,等待第一个线程执行结束,还是会从相同的位置产生一个新对象,就无法保证一个对象的原则


3.使用volatile关键字
volatile保证防止指令重排


八、阻塞队列

8.1生产消费者模型


8.2JDK中阻塞队列BlockingQueue


阻塞队列的大小一般由构造方法传入

8.3定时器常用方法

九、线程池 9.1线程池的作用


减少创建线程的时间和开销,只需要从系统中取出任务(run方法)就可以执行

9.2JDK线程池使用

9.3Executors线程池工具类

四种创建线程池方法



9.4ThreadPoolExector核心参数



动态缓存池:核心线程为0,每当有新任务进来都是临时创建线程

单线程池:
固定大小延迟池:

单线程池的意义

9.5线程池工作流程

十、常用锁的策略 10.1悲观锁 乐观锁

10.2乐观锁常用实现

开始时线程1和2先将主内存中的值100和版本号1读入自己各自的工作内存




如果尝试重新写回主内存成功就变为balance=30 version=3

10.3读写锁

10.4重量级锁和轻量级锁


10.5公平锁和非公平锁

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/1040827.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号