Java多线程总结
Posted 赵jc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程总结相关的知识,希望对你有一定的参考价值。
多线程
并行与并发
- 并发:多个进程在一个CPU下采用时间片轮转的方式,在一段时间之内,让多个进程都得以推进,称之为并发(实际上并没有同时进行)。
- 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发与并行类似于工厂中的流水线,要扩大产量,1是考虑建造多个工厂,这就是并行,2是考虑每个工厂中新增流水 线,这就类似并发。
上下文切换
多核cpu下,多线程是并行工作的,如果线程数多,单个核又会并发的调度线程,运行时会有上下文切换的概念cpu执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。
- 线程的cpu时间片用完
- 垃圾回收
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,jvm中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。
进程与线程
进程(系统资源分配得最小单位)
- 当一个程序被运行,就开启了一个进程, 比如启动了qq
- 程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备
线程(系统调度的最小单位)
- 一个进程内可分为多个线程(一个进程中最少有一个主线程)
- 一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令
进和线程的区别
多线程的好处
- 程序运行的更快!
- 充分利用cpu资源。
多线程的应用场景
- 工作量大,执行时间比较长的任务
- 让阻塞的代码不影响后续代码的执行(后续的代码在其他线程执行)
Thread类的常见方法
静态方法:作用在当前线代码所在的线程
- static Thread currentThread() 获取代码行所在的当前线程
- static void sleep(long millis) 让当前线程休眠给定的时间,会抛出InterruptedException异常
- static void **yield()**当前线程让步,从运行态变为就绪态
- static boolean interrupted() 判断当前线程的中断标志被设置,清除中断标志
实例构造方法:作用在调用的线程对象上
- void start() 启动线程,申请系统调度该线程
- void run()定义线程的任务
- void interrupt()中断一个线程
- booelan isIntrrupted()
- void join()无条件等待:当前线程阻塞并等待,一直等到调用的线程执行完毕,也可以传入一个参数,表示限时等待:当前线程阻塞并等待,直到调用线程执行完毕,或者时间到了,再往下执行
- boolean isAlive()是否存活,即简单的理解,为 run 方法是否运行结束了
- String getName()获取线程名称
- int getPriority() 获取线程优先级 0-10的数值
- boolean isDaemon()是否为后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
多线程的基本使用
线程的状态 state
大家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。
我们来举个例子:
- 刚把李四、王五找来,给他们在安排任务,没让他们行动起来,就是 NEW 状态;
- 当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE
状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度; - 当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入 BLOCKED 、WATING 、 TIMED_WAITING 状态
- 如果李四、王五已经忙完,为 TERMINATED 状态。
线程的创建 new
- 继承Thread类
// 方法一 继承Thread 单继承
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("子线程" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("主线程" + Thread.currentThread().getName());
}
- 实现Runnanle接口(重写run()方法)
//方法一 实现Runnable接口
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程名:" +
Thread.currentThread().getName());
}
}
public static void main(String[] args) {
// 创建 Runnable 子对象
MyRunnable myRunnable = new MyRunnable();
// 创建线程
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
//方法二 创建一个匿名 Runnable 常用
public static void main(String[] args) {
// 创建一个匿名 Runnable 类
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("当前线程:" +
Thread.currentThread().getName());
}
});
thread.start();
}
//方法三 lambda + runnable 常用
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("当前线程" + Thread.currentThread().getName());
});
thread.start();
}
- 实现Callable接口(重写call()方法)可以拿到线程的返回值(常用)
//方法以 可以拿到线程的返回值(常用)
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int num = new Random().nextInt(10);
System.out.println(String.format("线程%s 产生的随机数: %d",Thread.currentThread().getName(),num));
return num;
}
}
public static void main6(String[] args) throws ExecutionException, InterruptedException {
// 1.创建 Callable 子对象
MyCallable callable = new MyCallable();
// 2.使用 FutrueTask 接收 Callable
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 3.创建线程并设置任务
Thread thread = new Thread(futureTask);
// 执行线程
thread.start();
// 得到线程的执行结果
int ret = futureTask.get();
System.out.println("拿到的随机数为ret:" + ret);
}
线程的启动 start()
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
- 覆写 run 方法是提供给线程要做的事情
- 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
start()run()的区别:
- start()是线程的开启方法,它使用新的线程来执行任务,run()是一个对象的普通方法,它使用当前线程来执行任务
- start()方法可以执行一次,但run()可以调用多次
- 如果不调用start(),而是直接调用run(),相当于java对象直接调用普通的实例方法
线程的休眠 sleep()
休眠一个线程,会抛出InterredExcep异常
-
方式一
Thread.sleep( 1000);// 休眠 1 秒 -
方式二
TimeUnit.SECONDS.sleep(1); // 休眠 1 秒
TimeUnit.HOURS.sleep(1); // 休眠 1 小时 -
方式三
Thread.sleep(TimeUnit.SECONDS.toMillis(1));//休眠一秒
线程的等待 join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。
线程的中断 Interrupt
当线程进入运行态时,如果发生紧急情况,我们可以中断线程。
目前常见的有以下两种方式:
-
1.使用自定义的全局变量来终止(但当线程阻塞时不行终止线程,舍弃)
-
2.实例方法Thread.currentThread().interrupt() 设置代用线程的中断标志位为true,至于是否被中断,由当前线程决定(如果线程处于阻塞(调用wait/join/sleep),则会中断并且抛出InterruptedException异常,并重置标志位)
-
3.实例方法Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
-
4.静态方法Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志(静态的,大家都可以用,所以使用完后要恢复,方便下一次使用)
线程通信 wait()notify()
所谓的线程通信是指在一个线程中的操作可以影响到另一个线程。
wait()线程等待
其实wait()方法就是使线程停止运行。
- wait方法在执行前必须先加锁(wait配合synchronized一起使用)
- wait和notifiy在在配合使用时一定要操作同一把锁
- wait在不传递任何参数的情况下会进入waiting状态(其实底层调用了wait(0)这个方法),当传入一个大于0的整数时,它会进入timed_waiting状态
- wait在执行时会释放锁
notify()线程唤醒(随机唤醒一个)
notifyAll()线程唤醒(全部唤醒)
wait和sleep的区别
相同点
- wait和sleep都是让线程进入休眠状态
- wait和sleep在执行的过程中都可以接收到线程终止的通知
不同点
- wait必须配合synchronized一起使用,而sleep不用
- wait会释放锁,而sleep不会释放锁
- wait是Object的方法,而sleep是Thread(线程)的方法
- 默认情况下wait(不传递任何参数或者参数为0的情况下)它会进入waiting状态,而sleep会进入timed_waiting状态
- 使用wait时可以主动的唤醒线程,而使用sleep时不能主动地唤醒线程
sleep(0)和wait(0)的区别
- sleep(0)表示过了0ms之后会继续执行,而wait(0)会一直休眠
- sleep(0)会重新出发一次CPU竞争
为什么wait会释放锁,而sleep不会释放锁?
- sleep必须要传入一个最大等待时间,也就是说sleep是可控的(对于时间层面来说),而wait是不可以传递参数的,如果wait不主动释放锁的话就,没被唤醒前就会一直阻塞
为什么wait是Object的方法,而sleep是Thread的方法?
wait需要操作锁,而锁是属于对象级别的(存放在对象头当中)它不是线程级别的,一个线程可以有多把锁,为了灵活起见,所以讲wait放在了Object当中
解决wait/notify随机唤醒的问题(指定唤醒某个线程)
- LockSupport park()/unpark(线程)
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 让线程进行休眠
LockSupport.park();
System.out.println("唤醒 t1");
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 让线程进行休眠
LockSupport.park();
System.out.println("唤醒 t2");
}
}, "t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
// 让线程进行休眠
LockSupport.park();
System.out.println("唤醒 t3");
}
}, "t3");
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t2);
}
}
- LockSupporrt()虽然不会报Interrupt的异常,但依然可以监听到线程终止的指令
ublic class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("park 之前 Interrupt 状态:" +
Thread.currentThread().isInterrupted());
// 线程进入休眠
LockSupport.park();
System.out.println("park 之后 Interrupt 状态:" +
Thread.currentThread().isInterrupted());
}
}, "t1");
// 启动线程
t1.start();
Thread.sleep(100);
// 中止线程
t1.interrupt();
// 唤醒线程 t1
LockSupport.unpark(t1);
}
}
线程安全
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的,否则即为不安全的。
多线程不安全的原因
CPU是抢占式执行的(万恶之源)
非原子性
什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还 没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如 n++,其实是由三步操作组成的:
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
或者new 一个对象, ListNode tmp = new ListNode();
- 创建初始化内存空间
- new对象
- 赋值给变量
内存可见性
主内存-工作内存
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题
编译器优化/指令重排序
操作的是一个变量
解决线程不安全问题
volatile关键字
- 可以解决内存不可见和指令重排序的问题,
- 但不可以解决原子性的问题
使用场景: - 写操作不依赖共享变量,赋值的是一个常量(依赖共享变量是原子性操作)
- 作用在读,写依赖其他手段(加锁)
synchronized 关键字(监视器锁monitor lock)
synchronized的底层是使用操作系统的mutex lock实现的。(jvm层面来解决问题的)
- 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
synchronized用的锁是存在Java对象头里的(Java层面)。
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题; 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
synchronized实现:
- 针对操作系统层面,它是依靠互斥锁mutex
- 针对JVM层面,它是依靠monitor来实现
- 针对Java语言来说,是将锁信息存放在对象头(标识,锁的状态,所得拥有者)
synchronized的三种使用场景
- 使用synchronized修饰代码块(可以给任意对象进行加锁)
- 使用synchronized来修是静态方法(对当前的类进行加锁)
- 使用synchronized来修饰普通实例方法(对当前类实例进行加锁)
sunchronized锁升级的过程(JDK1.6以后)
lock()手动加锁
加锁的方式
lock的使用场景
- lock只能修饰代码块
注意事项:
lock()操作一定要放在try外面,如果放在try里面可能会造成两个问题:
- 如果try里面抛异常了,还没有加锁成功就执行了finally里面的释放所得操作(但此时还没有得到锁呢)
- 如果放在try里面,如果没有锁的情况下试图释放锁,这个时候产生的异常就会将业务代码(也就是try里面的异常)给覆盖掉,增加了代码调试的难度
公平锁和非公平锁
- 公平锁:一个线程释放锁,(主动)唤醒“需要得到锁”的就绪队列里的线程来得到锁
- 非公平锁:当一个线程释放锁之后,另一个线程刚好执行到获取锁的代码就直接可以获取锁(效率更高)
在Java语言中所有的锁默认都是非公平锁(synchronized和ReentrantLock()默认都是非公平锁),但lock可以显示声明公平锁
Lock lock = new ReentrantLock(true);
synchronized和lock的区别
- synchronized自行进行加锁和释放锁,二lock需要手动进行加锁和解锁
- lock是Java层面锁的实现的,二synchronized是JVM层面实现的
- synchronized可以修饰代码块、静态方法、实例方法,而lock只能修饰代码块
- synchronized只能实现非公平锁,但lock可以实现非公平锁和公平锁
- lock的灵活性更高(tryLock)
死锁
死锁:在两个或者两个以上的线程运行中,因为资源抢占而造成线程一直等待的问题。
简易的死锁代码
public class text {
public static void main(String[] args) {
//定义两个锁对象
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(lockA) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "得到lockA等待lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lockB) {
System.out.println("Wait B");
}
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "得到lockB等待lockA" );
synchronized(lockB) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lockA){
System.out.println("Wait A");
}
}
}
}, "t2");
t1.start();
t2.start();
}
}
造成死锁的四个条件:
- 互斥条件:当资源被一个线程拥有之后,就不能被其他的线程拥有了(不可更改)
- 请求拥有条件:当一个线程拥有了一个资源之后又试图请求另一个资源(可以解决)
- 不可剥夺条件:当一个资源被一个线程拥有之后,如果不是这个线程主动释放此资源的情况下,其他线程不能拥有此资源(不可更改)
- 环路等待条件:两个或两个以上的线程在拥有了资源之后,试图获取对方资源的时候形成了一个环路(可以解决)
如何解决死锁
- 控制加锁的顺序(解决环路的等待条件)
多线程案例
单例模式
- 饿汉模式
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()经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!